package org.zachary.imitation.raft;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors;

/**
 * 一个进程通常只会有一个节点，在 multi-raft-group 中，一个进程会有多个节点，它们的 ip 和 port 都是一样的，只是 groupId 不一样。NodeManager 就是用来管理一个进程中的多个节点的类
 */
public class NodeManager {
	/** 单例模式 */
	private static final NodeManager INSTANCE = new NodeManager();
	/** 存储一个进程中的每个节点的 Map */
	private final ConcurrentMap<NodeId, Node> nodeMap = new ConcurrentHashMap<>();
	/** key = groupId（集群 ID）, value = 该集群下的所有节点 */
	private final ConcurrentMap<String, List<Node>> groupMap = new ConcurrentHashMap<>();
	/** 存放一个进程中的所有节点的地址 */
	private final Set<Endpoint> addrSet = new HashSet<>();

	private NodeManager() {
	}

	public static NodeManager getInstance() {
		return INSTANCE;
	}

	/** 判断地址是否存在 */
	public boolean serverExists(final Endpoint addr) {
		return this.addrSet.contains(addr);
	}

	/** 从 addrSet 地址集合移除一个地址 */
	public boolean removeAddress(final Endpoint addr) {
		return this.addrSet.remove(addr);
	}

	/** 向 addrSet 地址集合中添加节点的地址 */
	public void addAddress(final Endpoint addr) {
		this.addrSet.add(addr);
	}

	/** 添加一个节点，该节点会被添加到 Map<NodeId, Node> 和 Map<String, List<Node>> 中 */
	public boolean add(final Node node) {
		if (!serverExists(node.getNodeId().getPeerId().getEndpoint())) {
			// addrSet 地址集合中不存在这样的地址，直接返回 false。在添加节点前，必须先调用 addAddress() 添加地址的方法
			return false;
		}

		final NodeId nodeId = node.getNodeId();
		// 将当前节点添加到 nodeMap 中。putIfAbsent 方法中，如果 nodeId 已经存在 Map 中了，则不会 put，会返回 value；如果不在，执行 put，并返回 null。即 putIfAbsent 方法会返回 Map 中 key 原来的 value
		if (this.nodeMap.putIfAbsent(nodeId, node) == null) {
			// 走到这里说明 Map 中之前不存在这样的 nodeId
			final String groupId = node.getGroupId();      // 获取节点所属的集群
			List<Node> nodes = this.groupMap.get(groupId); // 获取这个集群下所有的节点
			if (nodes == null) {
				// 将节点添加到集群的 groupMap 里。注意，这里可能会有多个线程同时执行 add(Node) 添加节点的操作，并同时进入 nodes == null 的分支。为了线程安全，所以下面使用了 groupMap.putIfAbsent

				// 创建一个线程安全的 ArrayList 来存放集群中的所有节点
				nodes = Collections.synchronizedList(new ArrayList<>());
				// 将集群中的所有节点放入到 groupMap 中
				List<Node> existsNodes = this.groupMap.putIfAbsent(groupId, nodes);
				if (existsNodes != null) {
					nodes = existsNodes;
				}
			}
			// 把当前节点添加到集群的集合中
			nodes.add(node);
			return true;
		}

		// 走到这里意味着 nodeMap 中已经存在对应的节点了，这里不会再次添加，所以返回 false
		return false;
	}

	/** 从 Map<NodeId, Node> 和 Map<String, List<Node>> 中移除一个节点 */
	public boolean remove(final Node node) {
		if (this.nodeMap.remove(node.getNodeId(), node)) {
			final List<Node> nodes = this.groupMap.get(node.getGroupId());
			if (nodes != null) {
				return nodes.remove(node);
			}
		}
		return false;
	}

	/** 已知集群 ID 和节点的 ip:port，从节点管理器中获取其对应的节点对象 */
	public Node get(final String groupId, final PeerId peerId) {
		return this.nodeMap.get(new NodeId(groupId, peerId));
	}

	/** 获取指定集群 ID 下所有的节点对象 */
	public List<Node> getNodesByGroupId(final String groupId) {
		return this.groupMap.get(groupId);
	}

	/** 获取节点管理器中纳管的所有的的节点对象 */
	public List<Node> getAllNodes() {
		return this.groupMap.values().stream().flatMap(Collection::stream).collect(Collectors.toList());
	}
}